package lichunUtil;

import java.io.PrintStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

/**
 *	to evaluate how much time passed. 
 * @author mark
 */
public class EvalTime {
	/**
	 * the PrintStream to show the results.
	 */
	private PrintStream out;
	/**
	 * record all fields that have already been started, 
	 * mapping them to the started time.
	 */
	private Map<String, Long> AllStartedField = new HashMap<String, Long>();
	/**
	 * record all fields that have already been paused, 
	 * mapping them to the timing from start till paused.
	 */
	private Map<String, Long> AllPausedField = new HashMap<String, Long>();
	/**
	 * record all fields' all period timing result, 
	 * mapping a field to an ArrayList which contains all periods result.
	 */
	private Map<String, ArrayList<Long>> AllPeriodTiming = new HashMap<String, ArrayList<Long>>();
	/**
	 * record all fields' timing results, up till now.
	 */
	private Map<String, Long> AllTiming = new HashMap<String, Long>();
	
	
	/**
	 * construct an EvalTime with the specified PrintStream.
	 * @param ps
	 * 		the PrintStream to show the results
	 */
	public EvalTime (PrintStream ps) {
		out = ps;
		if (ps==null) {
			System.err.println("the PrintStream should not be null");
			System.err.println("the timing results will show in \"Timing-result.txt\"");
			out = genTempPrintStream();
		}
	}
	/**
	 * construct an EvalTime with the specified file name.
	 * the PrintStream will be set to a default out if file not found.
	 * @param dpath
	 * the path of the file to show the results 
	 */
	public EvalTime (String dpath) {
		try {
			out = new PrintStream (dpath);
		} catch (Exception e) {
			e.printStackTrace();
			System.err.println("the timing results will show in \"Timing-result.txt\"");
			out = genTempPrintStream();
		}
	}
	/**
	 * construct an EvalTime with the standard output.
	 */
	public EvalTime () {
		out = System.out;
	}
	
	private PrintStream genTempPrintStream () {
		PrintStream ps =null;
		try {
			ps = new PrintStream ("Timing-result.txt");
		} catch (Exception e) {
			e.printStackTrace();
		}
		return ps;
	}
	
	/**
	 * create a timing field with the specified field name, 
	 * the field should not have been created
	 * @param field
	 * 		the unique name of the timing field
	 */
	public void createTimingField (String field) {
		createTimingField(field, false, false);
	}
	
	/**
	 * create a timing field with the specified field name.
	 * @param field
	 * 		the unique name of the timing field
	 * @param overwriteOld
	 * 		if true, will overwrite the old field if exists. 
	 * @param ignoreOld
	 * 		if true, will do nothing if the field exists.
	 */
	public void createTimingField (String field, boolean overwriteOld, boolean ignoreOld) {
		if (AllTiming.containsKey(field)) {
			if (overwriteOld) {
				AllPeriodTiming.put(field, new ArrayList<Long>());
				AllTiming.put(field, (long)0);
				return;
			}
			if (!ignoreOld)
				System.err.println("the timing field, "+field+","+" already exist");
			return;
		}
		AllPeriodTiming.put(field, new ArrayList<Long>());
		AllTiming.put(field, (long)0);
	}
	
	/**
	 * start a timing evaluation for the specified field.
	 * @param field
	 * 		the unique name of the timing field
	 * @param withCreate
	 * 		if true, will create the timing field if it does not exist.
	 * @param ignoreStarted
	 * 		if true, will reset the start point to now if the field has already been started.
	 * @return
	 * 		the current time millis.
	 */
	public long startTiming (String field, boolean withCreate, boolean ignoreStarted) {
		long now = System.currentTimeMillis();
		if (!AllTiming.containsKey(field)) {
			if (withCreate)
				createTimingField(field, false, false);
			else {
				System.err.println("the timing field, "+field+","+" does not exist");
				return now;
			}
		}
		if (!ignoreStarted && AllStartedField.containsKey(field)) {
			System.err.println("the timing field, "+field+","+" has already started");
			return now;
		}
		AllStartedField.put(field, now);
		
		// in case the field is paused, just omit it
		AllPausedField.remove(field);
		
		return now; 
	}

	/**
	 * start a timing evaluation for the specified field
	 * (withCreate and not ignoureStated).
	 * @param field
	 * 		the unique name of the timing field
	 * @return
	 * 		the current time millis
	 */
	public long startTiming (String field) {
		return startTiming(field, true, false);
	}
	
	/**
	 * end a period timing for the specified field, 
	 * and record the result to the PrintStream.
	 * @param field
	 * 		the unique name of the timing field
	 * @return
	 * 		the period timing result
	 */
	public long endTiming (String field) {
		return endTiming(field, true);
	}
	
	/**
	 * end a period timing for the specified field.
	 * @param field
	 * 		the unique name of the timing field
	 * @param record
	 * 		if true, the result will be written to the PrintStream.
	 * @return
	 * 		the period timing result
	 */	
	public long endTiming (String field, boolean record) {
		long now = System.currentTimeMillis();
		if (!AllTiming.containsKey(field)) {
			System.err.println("the timing field, "+field+","+" does not exist");
			return -1;
		}
		Long start = AllStartedField.get(field);
		if (start==null) {
			System.err.println("the timing field, "+field+","+" has not started");
			return -1;
		}
		AllStartedField.remove(field);
		
		long t;
		Long p = AllPausedField.get(field);
		if (p != null) {
			t = p;
			AllPausedField.remove(field);
		}
		else { 
			t = now - start;
		}
		long ori = AllTiming.get(field);
		AllTiming.put(field, ori + t);
		ArrayList<Long> list = AllPeriodTiming.get(field);
		list.add(t);
		return t;
	}
	
	
	/**
	 * end a period timing for the specified field, 
	 * record the result to the PrintStream, 
	 * and start a new period for the same field.
	 * @param field
	 * 		the unique name of the timing field
	 * @return
	 * 		the period timing result.
	 */
	public long end_restartTiming (String field) {
		return end_restartTiming(field, true);
	}

	/**
	 * end a period timing for the specified field, 
	 * record the result to the PrintStream, 
	 * and start a new period for the same field.
	 * @param field
	 * 		the unique name of the timing field
	 * @param record
	 * 		if true, the result will be written to the PrintStream.
	 * @return
	 * 		the period timing result.
	 */
	public long end_restartTiming (String field, boolean record) {
		long p = endTiming(field, record);
		startTiming(field, false, false);
		return p;
	}
	
	/**
	 * returns the total timing result of the specified field.
	 * @param field
	 * 		the unique name of the timing field
	 * @return
	 * 		the total timing result
	 */
	public long getTiming (String field) {
		Long res = AllTiming.get(field);
		if (res==null) {
			System.err.println("the timing field, "+field+","+" does not exist");
		}
		return res;
	}
	
	/**
	 * pause the timing of the specified field, 
	 * will do nothing if the field has not been started, 
	 * the field should not have already been paused.
	 * @param field
	 * 		the unique name of the timing field
	 * @return
	 * 		the current time millis
	 */
	public long pauseTiming (String field) {
		return pauseTiming(field, true, false);
	}
	
	/**
	 * pause the timing of the specified field.
	 * @param field
	 * 		the unique name of the timing field
	 * @param ignoreNotStarted
	 * 		if true, will do nothing if the field has not been started
	 * @param ignorePaused
	 * 		if true, will do nothing if the field has already been paused
	 * @return
	 * 		the current time millis
	 */
	public long pauseTiming(String field, boolean ignoreNotStarted, boolean ignorePaused) {
		long now = System.currentTimeMillis();
		if (!AllTiming.containsKey(field)) {
			System.err.println("the timing field, "+field+","+" does not exist");
			return -1;
		}
		if (!AllStartedField.containsKey(field)) {
			if (!ignoreNotStarted)
				System.err.println("the timing field, "+field+","+" has not started");
			return now;
		}
		if (!ignorePaused && AllPausedField.containsKey(field)) {
			System.err.println("the timing field, "+field+","+" already paused");
			return now;
		}
		Long start = AllStartedField.get(field);
		AllPausedField.put(field, now-start);
		return now;
	}
	
	/**
	 * restart timing of a paused field, 
	 * the field should be started, 
	 * do nothing if the field is not paused.
	 * @param field
	 * 		the unique name of the timing field
	 * @return
	 * 		the current time millis
	 */
	public long continueTiming (String field) {
		return continueTiming(field, false, true);
	}
	
	/**
	 * restart timing of a paused field.
	 * @param field
	 * 		the unique name of the timing field
	 * @param
	 * 		if true, will do nothing if the field is not started
	 * @param ignoreNotPaused
	 * 		if true, will do nothing if the field is not paused
	 * @return
	 * 		the current time millis
	 */
	public long continueTiming (String field, boolean ignoreNotStarted, boolean ignoreNotPaused) {
		long now = System.currentTimeMillis();
		if (!AllTiming.containsKey(field)) {
			System.err.println("the timing field, "+field+","+" does not exist");
			return -1;
		}
		if (!AllStartedField.containsKey(field)) {
			if (!ignoreNotStarted)
				System.err.println("the timing field, "+field+","+" has not started");
			return now;
		}
		if (!AllPausedField.containsKey(field)) {
			if (!ignoreNotPaused)
				System.err.println("the timing field, "+field+","+" is not paused");
			return now;
		}
		long passed = AllPausedField.get(field);
		AllPausedField.remove(field);
		AllStartedField.put(field, now - passed);
		return now;		
	}
	
	/**
	 * print all period timing result of the specified field.
	 * the timing unit is set to "second".
	 * @param field
	 * 		the unique name of the timing field
	 */
	public void printAllPeriodTiming (String field) {
		printAllPeriodTiming(field, "second");
	}
	
	/**
	 * print all period timing result of the specified field.
	 * @param field
	 * 		the unique name of the timing field
	 */
	public void printAllPeriodTiming (String field, String unit) {
		if (!AllTiming.containsKey(field)) {
			System.err.println("the timing field, "+field+","+" does not exist");
			return;
		}
		ArrayList<Long> allp = AllPeriodTiming.get(field);
		out.printf(FieldUpLine+"%n");
		out.printf("timing field: %s%n", field);
		for (int i=0;i<allp.size();i++) {
			long timing = allp.get(i);
			String info = genTimingInfo(timing, unit);
			out.printf("phase %3d: %s%n", i, info);
		}
		out.printf(FieldDownLine+"%n");
		out.flush();
	}
	
	/**
	 * generate a string that tells the timing
	 * with the specified timing unit
	 * if the unit is not defined, the result will be in second
	 * @param timing
	 * 		the timing result in millisecond
	 * @param unit
	 * 		the timing unit
	 * @return
	 * 		the string that tells the timing 
	 * 		with the specified timing unit 
	 */
	private String genTimingInfo (long timing, String unit) {
		String res = "";
		if (unit.equals("hour")) {
			res += String.format("%10.3f", (double)(double)timing/1000.0/3600.0);
			res += " hour";
			return res;
		}
		if (unit.equals("minute")) {
			res += String.format("%10.3f", (double)(double)timing/1000.0/60.0);
			res += " minute";
			return res;
		}
		
		//if (unit.equals("second")) {}
			res += String.format("%10.3f", (double)(double)timing/1000.0);
			res += " second";
		return res;
	}
	
	/**
	 * print the total timing result of the specified field.
	 * the timing unit is set to "second".
	 * @param field
	 * 		the unique name of the timing field
	 */
	public void printTotalTiming (String field) {
		printTotalTiming(field, "second");
	}
	
	/**
	 * print the total timing result of the specified field.
	 * @param field
	 * 		the unique name of the timing field
	 * @param unit
	 * 		the timing unit
	 */
	public void printTotalTiming (String field, String unit) {
		if (!AllTiming.containsKey(field)) {
			System.err.println("the timing field, "+field+","+" does not exist");
			return;
		}
		out.printf(FieldUpLine+"%n");
		long timing = AllTiming.get(field);
		String info = genTimingInfo(timing, unit);
		out.printf("timing field: %s%n", field);
		out.printf("%s%n", info);
		out.printf(FieldDownLine+"%n");
	}
	
	/**
	 * print all fields' total timing results.
	 * the timing unit is set to "second".
	 */
	public void printAllFieldTotalTiming () {
		printAllFieldTotalTiming("second");
	}
	/**
	 * print all fields' total timing results
	 * with the specified timing unit.
	 * @param unit
	 * 		the timing unit
	 */
	public void printAllFieldTotalTiming (String unit) {
		for (String field : AllTiming.keySet()) {
			printTotalTiming(field);
		}
	}
	
	/**
	 * print all fields' all period timing results.
	 * the timing unit is set to "second".
	 */
	public void printAllFieldAllPeriodTiming () {
		printAllFieldAllPeriodTiming("second");
	}
	/**
	 * print all fields' all period timing results
	 * with the specified timing unit.
	 * @param unit
	 * 		the timing unit
	 */
	public void printAllFieldAllPeriodTiming (String unit) {
		for (String field : AllPeriodTiming.keySet()) {
			printAllPeriodTiming(field);
		}
	}
	
	static private EvalTime globalEvalTime = null;
	static public void setGlobalEvalTime(String filepath) {
		globalEvalTime = new EvalTime(filepath);
	}
	static public EvalTime getGlobalEvalTime() {
		if (globalEvalTime==null)
			System.err.println("global evaltime has not been set");
		return globalEvalTime;
	}
	static private String FieldUpLine = "________________________________________";
	static private String FieldDownLine = "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~";
}